Skip to content

fix(invitations): revoke/duplicate guards + resend endpoint#3857

Merged
PierreBrisorgueil merged 6 commits into
masterfrom
fix/3834-invitation-ops-guards
Jun 13, 2026
Merged

fix(invitations): revoke/duplicate guards + resend endpoint#3857
PierreBrisorgueil merged 6 commits into
masterfrom
fix/3834-invitation-ops-guards

Conversation

@PierreBrisorgueil

Copy link
Copy Markdown
Contributor

Summary

Hardens the platform-invitation ops surface (admin tab + the /api/invitations API).

  • Revoke status-guard: revoke() now filters { _id, status: 'pending' }. Revoking an already-accepted invite (which corrupted referral attribution — it dropped out of findAccepted() used by the reconcile cron) now returns 409 Conflict ("Only pending invitations can be revoked") instead of silently flipping accepted → revoked. A benign revoke-mid-claim race is documented.
  • Duplicate-pending guard: create() rejects a second pending invite for the same email with 409 ("A pending invitation already exists for this email") — previously N zombie pending invites per email were possible.
  • Resend endpoint: POST /api/invitations/:invitationId/resend (admin CASL) re-sends the existing token (no regeneration) so a failed invite email is recoverable from the UI. 409 if the invite is non-pending, 422 if the mailer is unconfigured. Registered on the canonical mount and the legacy /api/auth/invitations alias for consistency.

Error-string casing follows the house errors.getMessage convention (getMessage-routed descriptions carry a trailing period; literal-string descriptions do not) — asserted at both unit and integration layers.

Test plan

  • invitations.repository/service/controller.unit + invitations.integration — 132 tests, 7 suites green. New integration cases: revoke-accepted→409, duplicate-pending→409, resend re-sends existing token, resend guards (409 non-pending / 422 mailer-off), resend via the legacy alias.

Guardrails check

  • npm run lint clean
  • Public-OSS clean (also neutralized a pre-existing downstream name in the touched auth.routes.js deprecation-alias comment)
  • Tests added for every new behavior

Closes #3834

Revoking an accepted invitation flipped it to revoked and silently
dropped it out of the accepted set used for referral attribution.
The repository CAS now filters status:'pending'; the controller maps
a null update on an existing invitation to 409 Conflict. The
revoke-mid-claim race is benign and documented at the CAS.

refs #3834
create() allowed N live tokens for the same email — any one of them
opens the signup gate. Guard on the existing pending-only findByEmail
(expired/revoked/accepted invites never block a re-invite) and thread
the 409 through the controller instead of the hardcoded 422.

refs #3834
POST /api/invitations/:invitationId/resend (admin CASL chain, also on
the legacy /api/auth/invitations alias for mount consistency).
Re-sends the signup-invite email with the EXISTING token — the list
endpoint strips tokens, so a lost email previously meant a dead
invite. 409 when not pending, 422 when the mailer is unconfigured;
the send is awaited (the email IS the operation).

refs #3834
Replaces a downstream consumer name in the alias-removal TODO with neutral

wording (public-OSS no-downstream-refs rule). refs #3834
Copilot AI review requested due to automatic review settings June 13, 2026 13:03
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@PierreBrisorgueil, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 13 minutes and 9 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4cc63e22-9d42-40c0-a21f-44a68798f08d

📥 Commits

Reviewing files that changed from the base of the PR and between 137f37e and 45c2c0f.

📒 Files selected for processing (9)
  • modules/auth/routes/auth.routes.js
  • modules/invitations/controllers/invitations.controller.js
  • modules/invitations/repositories/invitations.repository.js
  • modules/invitations/routes/invitations.routes.js
  • modules/invitations/services/invitations.service.js
  • modules/invitations/tests/invitations.controller.unit.tests.js
  • modules/invitations/tests/invitations.integration.tests.js
  • modules/invitations/tests/invitations.repository.unit.tests.js
  • modules/invitations/tests/invitations.service.unit.tests.js
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/3834-invitation-ops-guards

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.03%. Comparing base (137f37e) to head (45c2c0f).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3857      +/-   ##
==========================================
+ Coverage   91.99%   92.03%   +0.04%     
==========================================
  Files         160      160              
  Lines        5310     5337      +27     
  Branches     1708     1717       +9     
==========================================
+ Hits         4885     4912      +27     
  Misses        337      337              
  Partials       88       88              
Flag Coverage Δ
integration 60.10% <96.66%> (+0.18%) ⬆️
unit 72.64% <90.00%> (+0.10%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 137f37e...45c2c0f. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the invitations admin surface by preventing invalid state transitions (revoking accepted invites, duplicate pending invites) and adds a resend endpoint so admins can recover from failed invite email delivery without regenerating tokens.

Changes:

  • Add a duplicate-pending invitation guard in InvitationService.create() returning 409 Conflict.
  • Guard InvitationRepository.revoke() to only revoke pending invites (accepted invites now refuse with 409 Conflict).
  • Add POST /api/invitations/:invitationId/resend (and legacy /api/auth/invitations/:invitationId/resend) to resend the existing invite token email, with mailer/status guards.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
modules/invitations/services/invitations.service.js Adds shared invite email payload builder, duplicate-pending guard, and resend() service method.
modules/invitations/repositories/invitations.repository.js Guards revoke CAS on { _id, status: 'pending' } to prevent accepted→revoked corruption.
modules/invitations/controllers/invitations.controller.js Threads 409 from create, maps revoke non-pending to 409, and adds resend controller action.
modules/invitations/routes/invitations.routes.js Registers canonical resend endpoint under /api/invitations.
modules/auth/routes/auth.routes.js Registers legacy alias resend endpoint under /api/auth/invitations.
modules/invitations/tests/invitations.service.unit.tests.js Adds unit coverage for duplicate-pending and resend behavior/guards.
modules/invitations/tests/invitations.repository.unit.tests.js Updates revoke test to assert pending-only guard.
modules/invitations/tests/invitations.controller.unit.tests.js Adds controller-level coverage for 409 passthrough, revoke 409, and resend.
modules/invitations/tests/invitations.integration.tests.js Adds integration coverage for revoke-accepted 409, duplicate-pending 409, and resend (canonical + alias).

Comment thread modules/invitations/services/invitations.service.js
Comment thread modules/invitations/controllers/invitations.controller.js
Comment thread modules/invitations/services/invitations.service.js
resend()/create() now pass through any AppError status (e.g. a 404) instead

of flattening all-but-409 to 422; app.param anchored before its routes.

Addresses critical-review H1 + M1 on #3857. refs #3834
create() @throws now lists the 409 duplicate-pending case; the resend 404

message is capitalized to match invitationByID. Review feedback on #3857. refs #3834
@PierreBrisorgueil PierreBrisorgueil merged commit 26dc3ff into master Jun 13, 2026
8 checks passed
@PierreBrisorgueil PierreBrisorgueil deleted the fix/3834-invitation-ops-guards branch June 13, 2026 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🔧 invitation ops: copy-link/resend, revoke status-guard, duplicate-pending guard

2 participants